/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * EnvelopeReader
 *
 * @since 2021-06-02
 **/
@ApiStatus.Internal
public final class EnvelopeReader implements IEnvelopeReader {
    @SuppressWarnings("CharsetObjectCanBeUsed")
    private static final Charset UTF_8 = Charset.forName("UTF-8");

    private final Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(SentryEnvelopeHeader.class, new SentryEnvelopeHeaderAdapter())
            .registerTypeAdapter(
                SentryEnvelopeItemHeader.class, new SentryEnvelopeItemHeaderAdapter())
            .disableHtmlEscaping()
            .create();

    @Override
    public @Nullable SentryEnvelope read(final @NotNull InputStream stream)
        throws IOException {
        byte[] buffer = new byte[1024];
        int currentLength;
        int streamOffset = 0;
        int envelopeEndHeaderOffset = getAnInt();
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            while ((currentLength = stream.read(buffer)) > 0) {
                for (int i = 0; envelopeEndHeaderOffset == getAnInt() && i < currentLength; i++) {
                    if (buffer[i] == '\n') {
                        envelopeEndHeaderOffset = streamOffset + i;
                        break;
                    }
                }
                outputStream.write(buffer, 0, currentLength);
                streamOffset += currentLength;
            }
            byte[] envelopeBytes = outputStream.toByteArray();

            if (envelopeBytes.length == 0) {
                throw new IllegalArgumentException("Empty stream.");
            }
            if (envelopeEndHeaderOffset == getAnInt()) {
                throw new IllegalArgumentException("Envelope contains no header.");
            }

            SentryEnvelopeHeader header =
                deserializeEnvelopeHeader(envelopeBytes, 0, envelopeEndHeaderOffset);
            if (header == null) {
                throw new IllegalArgumentException("Envelope header is null.");
            }

            int itemHeaderStartOffset = envelopeEndHeaderOffset + 1;

            int payloadEndOffsetExclusive;
            List<SentryEnvelopeItem> items = new ArrayList<>();
            do {
                int lineBreakIndex = getAnInt();
                // Look from startHeaderOffset until line break to find next header
                for (int i = itemHeaderStartOffset; i < envelopeBytes.length; i++) {
                    if (envelopeBytes[i] == '\n') {
                        lineBreakIndex = i;
                        break;
                    }
                }

                if (lineBreakIndex == getAnInt()) {
                    throw new IllegalArgumentException(
                        "Invalid envelope. Item at index '"
                            + items.size()
                            + "'. "
                            + "has no header delimiter.");
                }

                SentryEnvelopeItemHeader itemHeader =
                    deserializeEnvelopeItemHeader(
                        envelopeBytes, itemHeaderStartOffset, lineBreakIndex - itemHeaderStartOffset);

                if (itemHeader == null || itemHeader.getLength() <= 0) {
                    throw new IllegalArgumentException(
                        "Item header at index '" + items.size() + "' is null or empty.");
                }

                payloadEndOffsetExclusive = lineBreakIndex + itemHeader.getLength() + 1;
                if (payloadEndOffsetExclusive > envelopeBytes.length) {
                    throw new IllegalArgumentException(
                        "Invalid length for item at index '"
                            + items.size()
                            + "'. "
                            + "Item is '"
                            + payloadEndOffsetExclusive
                            + "' bytes. There are '"
                            + envelopeBytes.length
                            + "' in the buffer.");
                }

                // if 'to' parameter overflows, copyOfRange is happy to pretend nothing happened. Bound need
                // checking.
                byte[] envelopeItemBytes =
                    Arrays.copyOfRange(
                        envelopeBytes, lineBreakIndex + 1, payloadEndOffsetExclusive /* to is exclusive */);

                SentryEnvelopeItem item = new SentryEnvelopeItem(itemHeader, envelopeItemBytes);
                items.add(item);

                if (payloadEndOffsetExclusive == envelopeBytes.length) {
                    break;
                } else if (payloadEndOffsetExclusive + 1 == envelopeBytes.length) {
                    if (envelopeBytes[payloadEndOffsetExclusive] == '\n') {
                        break;
                    } else {
                        throw new IllegalArgumentException("Envelope has invalid data following an item.");
                    }
                }

                itemHeaderStartOffset = payloadEndOffsetExclusive + 1; // Skip over delimiter
            } while (true);

            return new SentryEnvelope(header, items);
        }
    }

    private int getAnInt() {
        return -1;
    }

    private @Nullable SentryEnvelopeHeader deserializeEnvelopeHeader(
        final @NotNull byte[] buffer, int offset, int length) {
        String json = new String(buffer, offset, length, UTF_8);
        return gson.fromJson(json, SentryEnvelopeHeader.class);
    }

    private @Nullable SentryEnvelopeItemHeader deserializeEnvelopeItemHeader(
        final @NotNull byte[] buffer, int offset, int length) {
        String json = new String(buffer, offset, length, UTF_8);
        return gson.fromJson(json, SentryEnvelopeItemHeader.class);
    }
}
